package graphics;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.util.Iterator;
import java.util.Stack;

import logger.Logger;

public class Graphics3D {
	//
	// This will represent a 3D graphics engine
	//
	private	static	Matrix			M = Matrix.identity();
	private	static	Stack<Matrix>	render = new Stack<Matrix>();
	private static	IZbuffer		zbuffer = null;
	private	static	Graphics		G;
	private static	Color			color = Color.black;

	public Graphics3D(IZbuffer zbuffer) {
		this.setZbuffer(zbuffer);
	}

	public final void push(Matrix M2) {
		setCamera(getCamera().multiply(M2));
		render.push(M2);
	}
	public final void pop() {
		if (!render.isEmpty()) {
			Matrix	M2 = render.pop().inverse();
			setCamera(getCamera().multiply(M2));
		}
	}

	public final Matrix		getCamera()				{	return M;			}
	public final void 		setCamera(Matrix M2)	{	M = M2;				}

	public final Graphics	getGraphics()			{	return G;			}
	public final void		setGraphics(Graphics g)	{	G = g;				}

	public final float	linear(float x0, float y0, float x1, float y1, float x2) {
		if (x0==x1)	return y0;
		float	m = (y1-y0)/(x1-x0);
		float	b = y0-(m*x0);
		return	x2 * m + b;
	}


	public final void setPixel(int x, int y, float z, Color c)	{	zbuffer.setPixel(x, y, z, c);		}
	public final Color getPixel(int x, int y)					{	return zbuffer.getPixel(x, y);		}
	public final void drawPixel(int x, int y, float z)			{	setPixel(x,y,z,color);				}

	private final void drawLine(Vector P1, Vector P2) throws Exception {
		drawLine(P1, P2, getColor());
	}
	private final void drawLine(Vector P0, Vector P1, Color C) throws Exception {
		//
		// This should be called AFTER matrix transformation
		//
		int			x0	= (int) P0.getX();
		int			y0	= (int) P0.getY();
		float		z0	= P0.getX();
		int			x1	= (int) P1.getX();
		int			y1	= (int) P1.getY();
		float		z1	= P1.getZ();

		int 		dx = Math.abs(x1-x0);
		int 		dy = Math.abs(y1-y0);

		int 		sx, sy, err, e2;
		int			x, y;

		if (x0 < x1) {	sx = 1;	}	else	{ sx = -1;	}
		if (y0 < y1) {	sy = 1;	}	else	{ sy = -1;	}

		err = dx-dy;

		// Logger.println("LINE: " + P0.niceString() + " => " + P1.niceString());

		drawPixel(x0,y0,z0);	// plot(x0,y0)

		x = x0;
		y = y0;

		while(true) {
			if ((x == x1) && (y == y1))	break;
			e2 = 2*err;
			if (e2 > -dy) {
				err = err - dy;
				x = x + sx;
			}
			if ((x == x1) && (y == y1)) {
				float z = calcZ3(x0,y0,z0,x1,y1,z1,x,y);
				drawPixel(x,y,z);
				break;
			}
			if (e2 < dx) {
				err = err + dx;
				y = y + sy;
			}
		}
		// Finished drawing the line
	}

	private final void drawPolyOld(Polygon P) {
		if (P!=null && P.size()>0) {
			int		xPoints[] = new int[P.size()];
			int		yPoints[] = new int[P.size()];
			float	zPoints[] = new float[P.size()];

			int	i;
			Vector	T;
			for(i=0; i<P.size(); i++) {
				 T = P.get(i).transform(M);
				 xPoints[i] = (int) T.getX();
				 yPoints[i] = (int) T.getY();
				 zPoints[i] = (float) T.getZ();
			}
			getGraphics().drawPolygon(xPoints, yPoints, P.size());
		}
	}

	@SuppressWarnings("unused")
	private final void drawPoly(Polygon P) {
		drawPolyOld(P);
		if (P!=null && P.size()>0) {
			Vector	V1, V2;
			int		i;
			for(i=0; i<P.size(); i++) {
				V1 = P.get(i);
				V2 = P.get((i+1) % P.size());
				try {
					drawLine(V1,V2);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}
	}

//	private final void fillPolyOld(Polygon P) {
//		Logger.println("fillPolyOld");
//
//		if (P!=null && P.size()>0) {
//			int	xPoints[] = new int[P.size()];
//			int	yPoints[] = new int[P.size()];
//			int	i;
//			Vector	T;
//			for(i=0; i<P.size(); i++) {
//				 T = P.get(i).transform(M);
//				 xPoints[i] = (int) T.getX();
//				 yPoints[i] = (int) T.getY();
//			}
//			getGraphics().fillPolygon(xPoints, yPoints, P.size());
//		}
//	}

//	private final void fillPoly(Polygon P) {
//		Logger.println("Filling a polygon");
//	}
//	private final void renderPoly(Polygon P) {
//		fillPoly(P);	// Fills the texture of the polygon
//		drawPoly(P);	// Draws the wireframe of the polygon
//	}


	//
	// Used by render shape
	//
	public final void drawPlane(Plane P) {
		if (P!=null) {
			if (!P.isWire()) {
				Vector	Ta, Tb, Tc;
				Point	Pa, Pb, Pc;

				Ta = P.getA().transform(M);
				Tb = P.getB().transform(M);
				Tc = P.getC().transform(M);

				Pa = new Point((int) Ta.getX(), (int) Ta.getY());
				Pb = new Point((int) Tb.getX(), (int) Tb.getY());
				Pc = new Point((int) Tc.getX(), (int) Tc.getY());

				try {
					float CCW = Graphics2D.ccw(Pa, Pb, Pc);
					boolean visibleDraw = (CCW >= 0);
					visibleDraw = true;	// todo remove

					if (visibleDraw) {
						drawLine(Ta, Tb, P.getLineColor());
						drawLine(Tb, Tc, P.getLineColor());
						drawLine(Tc, Ta, P.getLineColor());
					}
				} catch (Exception e) {
					Logger.println("Tried to draw a plane but ccw threw an exception");
				}
			} else {
				Vector	Ta, Tb;

				Ta = P.getA().transform(M);
				Tb = P.getB().transform(M);
				try {
					drawLine(Ta, Tb, P.getLineColor());
				} catch (Exception e) {
					Logger.println("Tried to draw a wire but ccw threw an exception");
				}
			}
		}

	}
	public final void fillPlane(Plane P) {
		if (P!=null && P.size()>2) {
			Vector	Ta, Tb, Tc;
			// Vector Tcenter;
			Point	Pa, Pb, Pc;

			Ta = P.getA().transform(M);
			Tb = P.getB().transform(M);
			Tc = P.getC().transform(M);
			// Tcenter = P.getCenter().transform(M);

			Pa = new Point((int) Ta.getX(), (int) Ta.getY());
			Pb = new Point((int) Tb.getX(), (int) Tb.getY());
			Pc = new Point((int) Tc.getX(), (int) Tc.getY());


			try {
				float CCW = Graphics2D.ccw(Pa, Pb, Pc);
				boolean visibleFill = (CCW >= 0);
				visibleFill = true;

				if (visibleFill) {
					Vector	Tmin = P.getMinBounds().transform(M);
					Vector	Tmax = P.getMaxBounds().transform(M);
					int	x, y;
					int	minX = (int) Tmin.getX();
					int minY = (int) Tmin.getY();
					int maxX = (int) Tmax.getX();
					int maxY = (int) Tmax.getY();

					for(y=minY; y<maxY; y++) {
						boolean	wasInTriangle = false;
						for(x=minX; x<maxX; x++) {
							Point	pixel = new Point(x,y);
							float	z = calcZ4(
								Ta.getX(), Ta.getY(), Ta.getZ(),
								Tb.getX(), Tb.getY(), Tb.getZ(),
								Tc.getX(), Tc.getY(), Tc.getZ(),
								x, y
							);

							// Need to calculate it via interpolation
							if (Graphics2D.contains(Pa, Pb, Pc, pixel)) {
								setPixel(pixel.x, pixel.y, z, P.getFillColor());
								wasInTriangle = true;
							} else if (wasInTriangle) {
								break;	// Optimization to not extend beyond the far side of the triangle
							}
						}
					}
				} else {
				}
			} catch (Exception e) {
				Logger.println("Tried to draw a plane but ccw threw an exception");
			}
		}
	}
	public final void renderPlane(Plane P) {
		fillPlane(P);
		drawPlane(P);
	}

	//
	// Used to draw the shape
	//
	public final void drawShape(Shape S) {
		Iterator<Plane>	itr = S.iterator();
		while(itr.hasNext()) {
			Plane	P = itr.next();
			getGraphics().setColor(P.getLineColor());
			drawPlane(P);
		}
	}
	public final void fillShape(Shape S) {
		Iterator<Plane>	itr = S.iterator();
		while(itr.hasNext()) {
			Plane	P = itr.next();
			getGraphics().setColor(P.getFillColor());
			fillPlane(P);
		}
	}
	public final void renderShape(Shape S) {
		fillShape(S);	// Fills the testure of the polygon
		drawShape(S);	// Draws the wireframe of the polygon
	}

	public IZbuffer getZbuffer() {
		return zbuffer;
	}
	public void setZbuffer(IZbuffer zbuffer) {
		Graphics3D.zbuffer = zbuffer;
	}
	public Color getColor() {
		return color;
	}
	public void setColor(Color c) {
		color = c;
	}

	public int	getPixel(Color C) {
		return C.getRGB();
	}

	public void paintZbuffer(BufferedImage	image) {
		int		x, y;
		Color	C;
		int		pixel;

		for(x=0; x<image.getWidth(); x++) {
			for(y=0; y<image.getHeight(); y++) {
				C = zbuffer.getPixel(x,y);
				pixel = getPixel(C);
				image.setRGB(x, y, pixel);
			}
		}
	}

	public final float calcZ4(Vector V1, Vector V2, Vector V3, float x4, float y4) {
		return calcZ4(
			V1.getX(), V1.getY(), V1.getZ(),
			V2.getX(), V2.getY(), V2.getZ(),
			V3.getX(), V3.getY(), V3.getZ(),
			x4, y4
		);
	}
	private final float calcZ4(
		float x1, float y1, float z1,
		float x2, float y2, float z2,
		float x3, float y3, float z3,
		float x4, float y4
	) {
		// equation of a plane => Ax + By + Cz + D = 0;
		float	A = y1 * (z2-z3) + y2 * (z3-z1) + y3 * (z1-z2);
		float	B = z1 * (x2-x3) + z2 * (x3-x1) + z3 * (x1-x2);
		float	C = x1 * (y2-y3) + x2 * (y3-y1) + x3 * (y1-y2);
		float	D = -x1 * (y2*z3 - y3*z2) - x2 * (y3*z1 - y1*z3) - x3 * (y1*z2 - y2*z1);

		float	z4 = (D - A * x4 - B * y4) / C;

		return z4;
	}
	private final float calcZ3(
		float x1, float y1, float z1,
		float x2, float y2, float z2,
		float x3, float y3
	) {
		return calcZ4(x1,y1,z1,x2,y2,x2,z1,y1,z2,x3,y3);
	}
}


